Day 5 我們討論 SQL vs NoSQL,也用 Shopping Cart 設計了 Customer、Product、Cart、Order 與 Transaction。
現在假設我們選擇 PostgreSQL,而且 users Table 從幾千筆成長到:
1,000 rows
↓
1,000,000 rows
↓
100,000,000 rows
一個普通 Query:
SELECT *
FROM users
WHERE email = 'alvin@example.com';
可能開始變慢。
Database 要解決的問題是:
我要怎麼在大量資料中快速找到 Alvin?
這就是今天的主題:
Database Index
如果 email 沒有適合的 Index,Database 可能需要檢查大量 Rows:
Row 1 → not Alvin
Row 2 → not Alvin
Row 3 → not Alvin
...
Row 10,000,000
這類情況可以先理解成:
Full Table Scan / Sequential Scan
資料越多,需要讀取的資料可能越多。
假設有一本 1,000 頁的電話簿,要找:
Lin, Alvin
如果完全沒有排序:
Page 1
Page 2
Page 3
...
可能需要一直翻。
如果有索引:
A → Page 1
B → Page 50
...
L → Page 500
就可以快速縮小範圍。
Database Index 也是類似概念:
建立額外的資料結構,幫助 Database 更快定位資料。
例如:
CREATE INDEX idx_users_email
ON users(email);
概念上:
alvin@example.com
↓
Index
↓
找到 Row Location
↓
User Row
Index 不是免費的魔法。
Database 必須額外維護 Index:
Table Data
+
Index Data
當資料發生:
INSERT
UPDATE
DELETE
相關 Index 也可能需要更新。
所以:
Index 可以提升某些 Read Query,但會增加 Storage 與 Write Cost。
這就是第一個重要 Trade-off:
Read Performance ↑
Write Cost ↑
Storage ↑
Relational Database 的一般 Index 常使用 B-Tree family 的資料結構,實際實作依 Database 而不同。
Day 6 不需要先深入演算法細節。
先理解:
Tree-based Index 可以利用排序好的結構,快速縮小搜尋範圍。
它不只適合:
WHERE id = 123;
也很適合 Range Query:
WHERE age > 30;
或:
WHERE created_at
BETWEEN '2026-01-01' AND '2026-12-31';
因為 Index Key 有順序,可以找到起點後繼續讀取某個範圍。
不一定。
例如:
SELECT *
FROM users
WHERE email = 'alvin@example.com';
Email 幾乎每個 User 都不同,因此通常具有較高 Selectivity。
Index 往往很有價值。
但:
SELECT *
FROM users
WHERE is_active = true;
如果:
95% Users 都是 active
這個 Query 仍然需要大量 Rows。
Database Optimizer 可能判斷直接掃描 Table 更划算。
因此:
有 Index 不代表 Database 一定會使用它。
Optimizer 會考慮:
Statistics
Selectivity
Estimated Cost
Query Pattern
假設:
CREATE TABLE users (
id BIGINT PRIMARY KEY,
name VARCHAR(100),
email VARCHAR(255)
);
Database 通常會為 Primary Key 建立對應的 Unique Index。
因此:
SELECT *
FROM users
WHERE id = 123;
通常可以有效利用 Index。
這是非常值得準備的面試觀念。
假設常見 Query:
SELECT *
FROM orders
WHERE user_id = 123
AND status = 'PAID';
可以考慮:
CREATE INDEX idx_orders_user_status
ON orders(user_id, status);
這叫:
Composite Index
也就是:
(user_id, status)
一起建立 Index。
假設:
CREATE INDEX idx_orders_user_status
ON orders(user_id, status);
通常比較適合:
WHERE user_id = 123;
以及:
WHERE user_id = 123
AND status = 'PAID';
但只有:
WHERE status = 'PAID';
不一定能有效使用這個 Composite Index。
可以用電話簿理解。
如果排序方式是:
Last Name
↓
First Name
找:
Lin, Alvin
很方便。
只知道:
Last Name = Lin
也方便。
但只知道:
First Name = Alvin
就沒那麼方便。
這就是 Composite Index 常見的:
Leftmost Prefix
概念。
假設:
CREATE INDEX idx_employee
ON employees(department, salary);
SELECT *
FROM employees
WHERE department = 'IT';
SELECT *
FROM employees
WHERE department = 'IT'
AND salary > 100000;
SELECT *
FROM employees
WHERE salary > 100000;
一般來說:
A → 可以利用 Index
B → 可以很好地利用 Index
C → 不一定能有效利用這個 Composite Index
因為 Index 是從:
department
開始組織。
假設:
id
name
email
age
city
country
created_at
updated_at
status
全部建立 Index。
每次:
INSERT
Database 不只需要新增 Row,也可能需要更新多個 Index。
UPDATE / DELETE 同樣需要維護相關 Index。
所以:
More Indexes
↓
某些 Reads Faster
↓
Writes More Expensive
↓
More Storage
因此 Index 應該根據:
Real Query Pattern
設計,而不是「有 Column 就加 Index」。
不要:
Query 很慢
↓
直接加 Index
應該先:
Measure First
例如 PostgreSQL:
EXPLAIN
SELECT *
FROM users
WHERE email = 'alvin@example.com';
或:
EXPLAIN ANALYZE
SELECT *
FROM users
WHERE email = 'alvin@example.com';
可以幫助觀察:
使用什麼 Scan?
有沒有使用 Index?
預估讀多少 Rows?
實際花多少時間?
可能看到:
Seq Scan
或:
Index Scan
Performance Optimization 的習慣應該是:
Measure
↓
Find Bottleneck
↓
Optimize
↓
Measure Again
這是很適合延伸準備的題型。
不要只回答:
Add Index
可以分步驟分析。
整個 Request:
Client
↓
Network
↓
Backend
↓
Database
Latency 也可能來自:
Backend Logic
External API
Network
Serialization
所以先定位 Bottleneck。
檢查:
Execution Time
Rows Scanned
Query Plan
例如:
SELECT *
FROM orders
WHERE user_id = 123;
如果 user_id 是非常常見的 Filter,可以考慮:
CREATE INDEX idx_orders_user_id
ON orders(user_id);
不要習慣永遠:
SELECT *
如果只需要:
id
status
total_amount
就:
SELECT id, status, total_amount
FROM orders
WHERE user_id = 123;
另外也要注意:
N+1 Query
Unnecessary JOIN
Large Result Set
Missing Pagination
如果 Query 已經最佳化,但 Database 整體 Traffic 還是過高,才繼續考慮:
Cache
Read Replica
Partitioning
Sharding
這些會在後面的文章繼續展開。
延續 Day 5 Shopping Cart。
需求:
查詢某個 Customer 最近 20 筆已付款的 Orders。
Query:
SELECT *
FROM orders
WHERE customer_id = 123
AND status = 'PAID'
ORDER BY created_at DESC
LIMIT 20;
假設:
orders = 100,000,000 rows
我們可以開始思考 Composite Index:
CREATE INDEX idx_orders_customer_status_created
ON orders(customer_id, status, created_at DESC);
為什麼?
因為實際 Query Pattern 是:
customer_id = ?
status = ?
ORDER BY created_at
所以不是看到三個 Columns 就一定建立三個互不相關的 Index。
而是:
根據 Query Pattern 設計 Index。
例如:
SELECT *
FROM users
WHERE LOWER(email) = 'alvin@example.com';
如果只有普通的:
INDEX(email)
Database 不一定能直接有效使用它處理 Expression。
另一個例子:
WHERE name LIKE '%alvin%'
一般 B-Tree Index 通常也不適合用來快速定位這種前面帶 % 的搜尋。
所以:
Index 是否有效,取決於 Query 寫法、Index 類型以及 Database Optimizer。
目前 Architecture:
┌── Server #1
│
User → Load Balancer ├── Server #2
│
└── Server #3
↓
Database
前幾天:
Day 3 → Application Scaling
Day 4 → Load Balancing
Day 5 → Database Selection
Day 6 → Query Performance
System Design 很常是一個循環:
Find Bottleneck
↓
Understand Why
↓
Choose Solution
↓
Understand Trade-off
↓
Measure Again
今天可以練習:
1. Database Index 是什麼?
2. 為什麼 Index 可以讓 Query 變快?
3. 沒有 Index 時可能發生什麼?
4. Full Table Scan 是什麼?
5. B-Tree / B+ Tree 為什麼適合 Database Index?
6. Index 有什麼缺點?
7. 為什麼不能每個 Column 都建立 Index?
8. Primary Key 和 Index 有什麼關係?
9. Composite Index 是什麼?
10. 為什麼 Composite Index 的 Column Order 很重要?
11. Leftmost Prefix 是什麼?
12. Query 很慢時,你會怎麼 Debug?
13. EXPLAIN / EXPLAIN ANALYZE 是做什麼?
14. 加了 Index 還是不夠,下一步可以考慮什麼?
15. 1 億筆 Orders,要查某個 Customer 最近 20 筆
PAID Orders,你會怎麼設計 Index?
如果能不用背答案,用自己的話回答這些問題,就不只是知道:
Index = Faster Query
而是開始理解:
Query Pattern
Data Structure
Performance
Trade-off
核心概念:
Without Index
↓
可能掃描大量 Rows
With Index
↓
利用額外 Data Structure
↓
更快定位資料
但是:
Index ≠ Free Performance
因為需要:
Extra Storage
Write Maintenance
Index Maintenance
所以 Index Design 應該根據:
Query Pattern
Selectivity
Read / Write Ratio
Data Size
決定。
最重要的一句話:
不要因為 Query 慢就盲目加 Index。先 Measure、找到 Bottleneck,再根據真正的 Query Pattern 設計 Index。
假設 Product Page 每秒有:
100,000 Requests
即使 Database Query 已經很快,如果每個 Request 都直接打 Database:
100,000 Requests
↓
100,000 Database Queries
Database 還是可能成為 Bottleneck。
所以新的問題是:
如果很多 User 一直讀相同資料,真的每次都需要 Query Database 嗎?
下一篇:
Day 7|Cache:為什麼 Redis 可以讓系統快這麼多?
會開始了解:
Cache
Cache Hit
Cache Miss
Redis
TTL
Cache-Aside Pattern
Cache Invalidation
並延伸:
Cache 和 Database 不一致怎麼辦?
什麼資料適合 Cache?
Cache 掛掉會怎樣?
大量 Cache 同時失效會發生什麼?
Architecture 也會從:
Backend
↓
Database
進化成:
Backend
↓
Cache
↓
Database